import type { Node as AcornNode, Options as AcornOptions } from 'acorn'
import type { Dirent, Stats } from 'fs'
import { ILogger } from './logger'

export const AcornCodeKey = Symbol.for('complugin:acorn[code]')

export { AcornOptions, AcornNode }
export type AcornNodePri = AcornNode & { [AcornCodeKey]?: string }
export type SourceMapSegment = [number] | [number, number, number, number] | [number, number, number, number, number]
export interface SourceMap {
  file: string
  mappings: string
  names: string[]
  sourceRoot?: string
  sources: string[]
  sourcesContent?: string[]
  version: number
}

export interface ExistingDecodedSourceMap {
  file?: string
  mappings: SourceMapSegment[][]
  names: string[]
  sourceRoot?: string
  sources: string[]
  sourcesContent?: string[]
  version: number
}

export interface ExistingRawSourceMap {
  file?: string
  mappings: string
  names: string[]
  sourceRoot?: string
  sources: string[]
  sourcesContent?: string[]
  version: number
}

export type DecodedSourceMapOrMissing =
  | {
      mappings?: never
      missing: true
    }
  | ExistingDecodedSourceMap

export type SourceMapInput = ExistingRawSourceMap | string | null | undefined

export type Awaitable<T> = T | Promise<T>
export type TransformResult = string | { code: string; map?: SourceMapInput; ast?: AcornNode } | null | undefined | void
export type LoadResult =
  | TransformResult
  | { code: string; map?: SourceMap; copy: true; name?: string; fileName?: string }
export type ExternalIdResult = { id: string; external?: boolean }
export type ResolveIdResult = string | ExternalIdResult | EmitAssetArgs | null | undefined | void

export interface EmitAssetArgs {
  fileName?: string
  name?: string
  source?: Awaitable<string | Uint8Array | { code: string | Uint8Array; map?: SourceMap }>
}

export interface OutputFile {
  fileName: string
  contents: Uint8Array
  text: string
  meta?: {
    readonly imports?: string[] | { path: string }[]
    readonly exports?: string[]
    readonly isEntry?: boolean
    readonly entryPoint?: string
  }
}

export interface InputFileSystem {
  readFile(fileName: string): Promise<Buffer | string>
  readlink(fileName: string): Promise<string>
  readdir(dirname: string): Promise<Dirent[]>
  stat(fileName: string): Promise<Stats | undefined>
  lstat(fileName: string): Promise<Stats | undefined>
  realpath(fileName: string): Promise<string | undefined>
}

export interface CompluginBaseContext {
  logger: ILogger
}

export interface CompluginMinifyContext extends CompluginBaseContext {
  emitFile(asset: EmitAssetArgs): void
  /**
   * @deprecated instead of 'logger.error'
   */
  error(err: string): void | never
  /**
   * @deprecated instead of 'logger.warn'
   */
  warn(warning: string): void
  resolve(importee: string, importer?: string): Promise<{ id: string; external?: boolean } | undefined>
  parse(input: string, options?: AcornOptions): AcornNode
}

type AssetPlaceholder = string

export interface CompluginContext extends CompluginMinifyContext {
  readonly sourceMap: boolean
  addWatchFile(fileName: string): void
  emitStatic(fileName: string): AssetPlaceholder
  emitAsset(asset: EmitAssetArgs): AssetPlaceholder
  emitChunk(id: string): Promise<AssetPlaceholder>
  emitIife(id: string): Promise<AssetPlaceholder>
  rebuild(): void
}

export interface CompluginTransformContext extends CompluginContext {
  readonly isEntry: boolean
  error(err: string, pos?: number | { column: number; line: number }): void | never
  warn(warning: string, pos?: number | { column: number; line: number }): void
}

export interface CompluginOptions {
  readonly buildStart?: (this: CompluginMinifyContext) => Awaitable<void>
  readonly buildEnd?: (this: CompluginMinifyContext) => Awaitable<void>
  readonly transformInclude?: (this: CompluginBaseContext, id: string) => boolean
  readonly transform?: (
    this: CompluginTransformContext,
    code: string,
    id: string,
    ast?: AcornNode
  ) => Awaitable<TransformResult>
  readonly load?: (this: CompluginContext, id: string) => Awaitable<LoadResult>
  readonly resolveId?: (this: CompluginMinifyContext, importee: string, importer?: string) => Awaitable<ResolveIdResult>
  readonly generateBundle?: (
    this: CompluginBaseContext,
    bundle: Record<string, OutputFile>,
    rawBundle: any
  ) => Awaitable<void>
}

export interface CompluginContextMetaData {
  [k: keyof any]: any
}

export interface BaseCompluginContextMeta {
  readonly logger: ILogger
  readonly watchMode: boolean
  readonly version: string
  readonly data: CompluginContextMetaData
  readonly root: string
  readonly isProd: boolean
}

export interface CompluginGeneratorMap<UserOptions = {}> {
  vite(
    userOptions: UserOptions | undefined,
    meta: BaseCompluginContextMeta & {
      readonly server?: import('vite').ViteDevServer
      readonly previewServer?: Parameters<
        Extract<import('vite').Plugin['configurePreviewServer'], (...args: any[]) => any>
      >[0]
      readonly config: import('vite').ResolvedConfig
      readonly env: import('vite').ConfigEnv
      readonly plugin: import('vite').Plugin
      readonly inputOptions: import('rollup').NormalizedInputOptions
      readonly outputOptions?: import('rollup').NormalizedOutputOptions
    }
  ): import('vite').Plugin
  rollup(
    userOptions: UserOptions | undefined,
    meta: BaseCompluginContextMeta & {
      readonly base?: string
      readonly plugin: import('rollup').Plugin
      readonly inputOptions: import('rollup').NormalizedInputOptions
      readonly outputOptions?: import('rollup').NormalizedOutputOptions
    }
  ): import('rollup').Plugin
  webpack(
    userOptions: UserOptions | undefined,
    meta: BaseCompluginContextMeta & {
      readonly compiler: import('webpack').Compiler
      // @ts-ignore
      readonly devServer?: import('webpack-dev-server')
    }
  ): import('webpack').WebpackPluginInstance
  esbuild(
    userOptions: UserOptions | undefined,
    meta: BaseCompluginContextMeta & {
      readonly build: import('esbuild').PluginBuild & {
        onTransform(fn: (code: string, id: string, ast?: AcornNode) => Awaitable<TransformResult>): void
        onGenerate(fn: (bundle: Record<string, OutputFile>) => Awaitable<void>): void
      }
      /**
       * @deprecated instead of 'onResolveInclude'
       */
      onResolveRegExp?: RegExp
      /**
       * @deprecated instead of 'addLoadInclude'
       */
      onLoadRegExp?: RegExp
      onResolveInclude?: string | RegExp | Array<string | RegExp>
      addLoadInclude: (include: string | RegExp | Array<string | RegExp>) => void
    }
  ): import('esbuild').Plugin
}

export type CompluginContextMeta = {
  [F in keyof CompluginGeneratorMap]: CompluginGeneratorMap[F] extends {
    (userOptions: object | undefined, meta: infer Meta): any
  }
    ? Meta extends BaseCompluginContextMeta
      ? Meta & { framework: F; inputFS: InputFileSystem }
      : never
    : never
}[keyof CompluginGeneratorMap]

export type CompluginFactory<UserOptions = {}> = (
  this: CompluginContextMeta,
  userOptions: UserOptions | undefined,
  meta: CompluginContextMeta
) => CompluginOptions

type CompluginGenerators<UserOptions> = {
  [F in Exclude<keyof CompluginGeneratorMap, '$complugin' | 'raw'>]: (
    options?: UserOptions
  ) => ReturnType<CompluginGeneratorMap<UserOptions>[F]>
}

export interface CompluginInstance<UserOptions = {}> extends CompluginGenerators<UserOptions> {
  $complugin: CompluginInstance<UserOptions>
  raw: CompluginFactory<UserOptions>
  (userOptions?: UserOptions): { $complugin: CompluginInstance<UserOptions> } & {
    [F in Exclude<keyof CompluginGeneratorMap<UserOptions>, '$complugin' | 'raw'>]: ReturnType<
      CompluginGeneratorMap<UserOptions>[F]
    >
  }
}

export interface CreateCompluginArgs<UserOptions = {}> {
  name: string
  factory: CompluginFactory<UserOptions>
  enforce?: 'post' | 'pre'
}

export type CreateComplugin = <UserOptions = {}>(
  args: CreateCompluginArgs<UserOptions>
) => CompluginInstance<UserOptions>

export type RegisterCompluginGenerator = (
  framework: string,
  generator: <UserOptions = {}>(args: CreateCompluginArgs<UserOptions>, userOptions?: UserOptions) => any
) => void
